using System.Collections.Generic;
using GameFramework.Camera;
using GameFramework.Controller;
using GameFramework.GameLogic;
using GameFramework.Geometry;
using GameFramework.Materials;
using GameFramework.ModelManager;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MyRaceGame.GameLogic;
using MyRaceGame.GameLogic.InputControl;
using MyRaceGame.GameLogic.GameStates;
using Microsoft.Xna.Framework.GamerServices;
using System;
using GameFramework.Audio;
using System.IO;

namespace MyRaceGame
{
    /// <summary>
    /// Game class representing Race Game
    /// </summary>
    public class RaceGame : Game, IGameStateContext
    {
        /// <summary>
        /// Creates new instance of RaceGame via Instance property
        /// </summary>
        private RaceGame()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            // Set resolution
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 800;

            // Set singleton instance
            instance = this;
        }

        bool takeScreenShot = false;

        public void ToggleFullScreen()
        {
            Graphics.ToggleFullScreen();
            resetDisplayNameTextures();
        }

        void resetDisplayNameTextures()
        {
            RacerCharacter ndrChar;

            for (int i = 0; i < raceController.Racers.Count; ++i)
            {
                ndrChar = raceController.Racers[i];

                while (ndrChar is NameDisplayedRacerCharacter == false)
                {
                    ndrChar = (ndrChar as RacerDecorator).DecoratedRacerCharacter;
                }

                ((NameDisplayedRacerCharacter)ndrChar).ResetDisplayNameTexture();
            }
        }

        #region Load Methods

        /// <summary>
        /// Load different cameras
        /// </summary>
        void loadCameras()
        {
            cameras.Add(new ChaseCamera(new Vector3(20, 700, 400), new Vector3(0, 100, -200),
                new Vector3(MathHelper.Pi / 16, MathHelper.Pi / 8, 0), GraphicsDevice));
            cameras.Add(new FreeCamera(new Vector3(1000, 650, 1000),
                MathHelper.ToRadians(45),
                MathHelper.ToRadians(-30),
                GraphicsDevice));
        }

        /// <summary>
        /// Creates racer character objects from loaded assets
        /// </summary>
        void loadRacers()
        {
            // Base elliptic track to extend and to use for human racer
            Ellipse humanEllipse = new Ellipse(Vector2.Zero, 2600, 4600, 100);
            EllipticRaceTrack ellipticRaceTrack = new EllipticRaceTrack(humanEllipse,
                100, 1000, GraphicsDevice, raceTileAsset);

            // Add Human racer
            raceController.AddRacer(new RacerCharacter(hulki, Vector3.Zero, new Vector3(0, MathHelper.Pi, 0), 
                new Vector3(150), GraphicsDevice, "Hulki", ellipticRaceTrack, "Wait", "Walk", "Run", true));

            // Add CPU racer
            raceController.AddRacer(new RacerCharacter(dude, Vector3.Zero, Vector3.Zero,
                new Vector3(5), GraphicsDevice, "Kadri", ellipticRaceTrack.GetExtendedRaceTrack(600), "Take 001",
                "Take 001", "Take 001", false, 2000, 200));

            // Add a CPU Racer attached with guns in both hands and has AI capabilty
            raceController.AddRacer
            (
                    new AttachedRacerCharacter(
                        new AttachedRacerCharacter(
                            new RacerCharacter(dude, Vector3.Zero, Vector3.Zero, new Vector3(5), GraphicsDevice, 
                                "Bahri", ellipticRaceTrack.GetExtendedRaceTrack(300), "Take 001", "Take 001", "Take 001",
                                false, 1800, 150),
                        gun, "R_Hand"),
                    gun, "L_Hand")
                
            );

            // Add another CPU racer to the left of the human racer
            raceController.AddRacer(new RacerCharacter(dude, Vector3.Zero, Vector3.Zero,
                new Vector3(5), GraphicsDevice, "Fahri", ellipticRaceTrack.GetExtendedRaceTrack(-300), "Take 001",
                "Take 001", "Take 001", false, 1400, 50));

            raceController.AddRacer(new RacerCharacter(dude, Vector3.Zero, Vector3.Zero,
                new Vector3(5), GraphicsDevice, "Sadri", ellipticRaceTrack.GetExtendedRaceTrack(-600), "Take 001",
                "Take 001", "Take 001", false, 1200, 20));

            // Add another CPU racer with different model
            raceController.AddRacer(new RacerCharacter(soldier, Vector3.Zero, new Vector3(0, MathHelper.Pi, 0),
               new Vector3(250), GraphicsDevice, "Sabri", ellipticRaceTrack.GetExtendedRaceTrack(800), "Idle",
               "Idle", "Idle", false, 3000));

            // Add another CPU racer with different model
            raceController.AddRacer(new RacerCharacter(soccer, Vector3.Zero, new Vector3(0, MathHelper.Pi, 0),
               new Vector3(250), GraphicsDevice, "Madri", ellipticRaceTrack.GetExtendedRaceTrack(-800), "Jump",
               "Jump", "Jump", false, 500));

            attachAI();

            attachText();

            // Decorates racers with TimeCountedRacer decorator class
            attachTimer();

            // Start racers with their defined "Idle" animation
            raceController.InitializeRacers();

            // Track human player at start
            trackedRacer = raceController.HumanRacer;
        }

        /// <summary>
        /// Loads of 3D and 2D assets
        /// </summary>
        protected override void LoadContent()
        {
            // Get sprite batch from the device
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // Load cameras
            loadCameras();

            // Set current camera mode to Chase Camera which is default
            currentCamera = cameras[0];

            // Create input controller
            inputController = new InputController();

            // Create race controller with 5 laps
            raceController = new RaceController(4);

            // Load calibri font
            font = Content.Load<SpriteFont>(@"Fonts\calibri");

            // Add ground model 40 unit lower to static models
            staticModels.Add(new StaticModel(Content.Load<Model>(@"Models\Ground\Ground"), new Vector3(0, -41f, 0),
                Vector3.Zero, Vector3.One * 2, GraphicsDevice));

            // Load model assets
            dude = Content.Load<Model>(@"Models\Dude\dude");
            hulki = Content.Load<Model>(@"Models\Hulki\hulki");
            gun = Content.Load<Model>(@"Models\Mp5\mp5");
            soldier = Content.Load<Model>(@"Models\Soldier\soldier");
            soccer = Content.Load<Model>(@"Models\Soccer\sport");

            // Add effects to MP5 gun (external texture and default light
            foreach (ModelMesh mesh in gun.Meshes)
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.Texture = Content.Load<Texture2D>(@"Models\Mp5\mp5_tex");
                    effect.TextureEnabled = true;
                }

            // Load track texture
            raceTileAsset = Content.Load<Texture2D>(@"Tracks\track_me");

            // Load racers
            loadRacers();

            // Add effects to all static models
            foreach (StaticModel model in staticModels)
                foreach (ModelMesh mesh in model.Model.Meshes)
                    foreach (BasicEffect effect in mesh.Effects)
                    {
                        effect.LightingEnabled = true;
                        effect.PreferPerPixelLighting = true;
                        effect.AmbientLightColor = new Vector3(.4f);
                        effect.DirectionalLight0.Enabled = true;
                        effect.DirectionalLight0.DiffuseColor = new Vector3(.6f);
                        effect.DirectionalLight0.Direction = new Vector3(1, -1, -1);
                        effect.DirectionalLight0.Direction.Normalize();
                        effect.DirectionalLight0.SpecularColor = Color.Black.ToVector3();
                    }
            
            // Load skysphere
            skySphereModel = Content.Load<Model>(@"Models\Sky\skysphere_mesh");
            skySphereEffect = Content.Load<Effect>(@"Models\Sky\skysphere_effect");
            SkySphere = new SkySphere(Content, GraphicsDevice, Content.Load<TextureCube>(@"Models\Sky\clouds"),
                skySphereModel, skySphereEffect);

            //List<Vector3> treePositions = new List<Vector3>();
            
            //Random r = new Random();

            // Continue until we get 500 trees on the terrain
            //for (int i = 0; i < 500; i++) // 500
            //{
            //    treePositions.Add(new Vector3((float)r.NextDouble() * 1500f, 100, (float)r.NextDouble() * 1500));
            //}

            //trees = new BillboardSystem(GraphicsDevice, Content,
            //    Content.Load<Texture2D>(@"Models\Tree\tree_billboard"), new Vector2(500),
            //    treePositions.ToArray());

            //trees.Mode = BillboardSystem.BillboardMode.Cylindrical;
            //trees.EnsureOcclusion = true;
            
            // Create a 2D text drawing controller with calibri font and white color
            textDrawController = new TextDrawController(this, spriteBatch, font, Color.White);
            
            // Set first state as "Loading State"
            currentState = new LoadingState(this);

            //audioManager = new AudioManager(this);
            //audioManager.AddSoundEffect(@"Audio\run");
            //Components.Add(audioManager);

            PresentationParameters pp = GraphicsDevice.PresentationParameters;

            tex = new RenderTarget2D(GraphicsDevice,
                    pp.BackBufferWidth, pp.BackBufferHeight, true, GraphicsDevice.DisplayMode.Format,
                    DepthFormat.Depth24);
        }

        #endregion

        #region Update Methods

        /// <summary>
        /// Handles keyboard in general (independent from states)
        /// </summary>
        void updateKeyboard()
        {
            if (inputController.KeyPressed(Keys.Escape)) 
                Exit();

            if (inputController.KeyPressed(Keys.P))
                Paused = !Paused;

            if (cameraMode != MyRaceGame.CameraMode.FreeCamera && inputController.KeyPressed(Keys.S))
                takeScreenShot = true;

            if (cameraMode == MyRaceGame.CameraMode.FreeCamera && inputController.KeyPressed(Keys.Z))
                takeScreenShot = true;
        }



        /// <summary>
        /// Updates CPU racers and human racer with input controller
        /// </summary>
        /// <param name="gameTime"></param>
        public void UpdateRacers(GameTime gameTime, bool updateHumanSpeed = true)
        {
            if (updateHumanSpeed)
            {
                float speed = InputController.Speed * 1000 * 1f;
                raceController.HumanRacer.Speed = MathHelper.Clamp(speed, 0, inputController.MaxSpeed);
            }

            raceController.UpdateRacers(gameTime);
        }

        /// <summary>
        /// Updates when chase camera is active
        /// </summary>
        /// <param name="gameTime"></param>
        void updateChaseCamera(GameTime gameTime)
        {
            ((ChaseCamera)CurrentCamera).Move(trackedRacer.Position, new Vector3(0, trackedRacer.Rotation.Y +
                trackedRacer.InitialRotation, 0));
            ((ChaseCamera)CurrentCamera).Update();
        }

        /// <summary>
        /// Updates when free camera is active
        /// </summary>
        /// <param name="gameTime"></param>
        void updateFreeCamera(GameTime gameTime)
        {
            // Determine how much the camera should turn
            float deltaX = InputController.MouseDeltaX;
            float deltaY = InputController.MouseDeltaY;

            // Rotate the camera
            ((FreeCamera)CurrentCamera).Rotate(deltaX * .005f, deltaY * .005f);

            Vector3 translation = Vector3.Zero;

            // Determine in which direction to move the camera
            if (inputController.IsKeyDown(Keys.W)) 
                translation += Vector3.Forward;

            if (inputController.IsKeyDown(Keys.S)) 
                translation += Vector3.Backward;

            if (inputController.IsKeyDown(Keys.A))
                translation += Vector3.Left;

            if (inputController.IsKeyDown(Keys.D)) 
                translation += Vector3.Right;

            // Move 4 units per millisecond, independent of frame rate
            translation *= 0.5f *
                (float)gameTime.ElapsedGameTime.TotalMilliseconds;

            // Move the camera
            ((FreeCamera)CurrentCamera).Move(translation);

            // Update the camera
            CurrentCamera.Update();
        }
              
        protected override void Update(GameTime gameTime)
        {
            // Tell the AudioManager about the new camera position.
            //audioManager.Listener.Position = ((ChaseCamera)currentCamera).Position;
            //audioManager.Listener.Forward = Vector3.Forward;
            //audioManager.Listener.Up = Vector3.Up;
            //audioManager.Listener.Velocity = Vector3.Zero;

            if (delayEnabled)
            {
                delayMilliseconds -= gameTime.ElapsedGameTime.Milliseconds;

                if (delayMilliseconds > 0)
                    return;

                delayEnabled = false;
            }

            updateKeyboard();
            inputController.Update(gameTime);

            if (Paused)
                return;
            
            currentState.Update(gameTime);

            switch (cameraMode)
            {
                case CameraMode.FreeCamera:
                    updateFreeCamera(gameTime);
                    break;
                case CameraMode.ChaseCamera:
                    updateChaseCamera(gameTime);
                    break;
            }

            base.Update(gameTime);
        }

        #endregion

        #region Draw Methods

        /// <summary>
        /// Draws static and animated models
        /// </summary>
        public void DrawModels()
        {
            switch (cameraMode)
            {
                case CameraMode.ChaseCamera:
                    SkySphere.Draw(CurrentCamera.View, CurrentCamera.Projection, ((ChaseCamera)CurrentCamera).Position);
                    foreach (StaticModel model in StaticModels)
                        if (CurrentCamera.BoundingVolumeIsInView(model.BoundingSphere))
                            model.Draw(CurrentCamera.View, CurrentCamera.Projection,
                                ((ChaseCamera)CurrentCamera).Position);

                    RaceController.DrawRacers(CurrentCamera.View, CurrentCamera.Projection,
                        ((ChaseCamera)CurrentCamera).Position);
                    break;

                case CameraMode.FreeCamera:
                    SkySphere.Draw(CurrentCamera.View, CurrentCamera.Projection, ((FreeCamera)CurrentCamera).Position);
                    foreach (StaticModel model in StaticModels)
                        if (CurrentCamera.BoundingVolumeIsInView(model.BoundingSphere))
                            model.Draw(CurrentCamera.View, CurrentCamera.Projection,
                                ((FreeCamera)CurrentCamera).Position);

                    RaceController.DrawRacers(CurrentCamera.View, CurrentCamera.Projection,
                        ((FreeCamera)CurrentCamera).Position);
                    break;
            }
        }

        /// <summary>
        /// Draws the race track
        /// </summary>
        public void DrawRaceTrack()
        {
            raceController.HumanRacer.RaceTrack.Draw(CurrentCamera.View, CurrentCamera.Projection);
        }

        RenderTarget2D tex;

        /// <summary>
        /// Draws the game
        /// </summary>
        /// <param name="gameTime"></param>
        protected override void Draw(GameTime gameTime)
        {
            if (takeScreenShot)
                GraphicsDevice.SetRenderTarget(tex);

            GraphicsDevice.Clear(Color.CornflowerBlue);
            
            // Draw the current state
            currentState.Draw(gameTime);
            
            
            // Draw components
            base.Draw(gameTime);

            if (takeScreenShot)
            {
                restoreGraphicsDevice();
                GraphicsDevice.SetRenderTarget(null);

                FileStream fs = null;
                StreamWriter sw = null;

                try
                {
                    fs = new FileStream("screen" + DateTime.Now.Day + "-" + DateTime.Now.Month + "-" +
                        DateTime.Now.Year + "-" + DateTime.Now.TimeOfDay.ToString("hhmmss") + ".png", 
                        FileMode.Create, FileAccess.Write);
                    sw = new StreamWriter(fs);

                    tex.SaveAsPng(fs, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
                }
                finally
                {
                    if (sw != null)
                        sw.Close();
                }

                takeScreenShot = false;
            }
            else
                restoreGraphicsDevice();
        }

        #endregion

        #region Other Methods

        /// <summary>
        /// Changes camera mode and current camera object
        /// </summary>
        /// <param name="mode">Desired camera mode</param>
        public void ChangeCameraMode(CameraMode mode)
        {
            cameraMode = mode;
            CurrentCamera = cameras[(int)mode];
        }

        /// <summary>
        /// Changes tracked racer to next one in the list in a loop
        /// </summary>
        public void ChangeTrackedRacer()
        {
            List<RacerCharacter> racers = RaceController.Racers;

            trackedRacerIndex = (trackedRacerIndex + 1) % racers.Count;

            trackedRacer = racers[trackedRacerIndex];
        }

        /// <summary>
        /// Decorates racers with TimeCountedRacer decorator class
        /// </summary>
        void attachTimer()
        {
            for (int i = 0; i < raceController.Racers.Count; ++i)
                raceController.Racers[i] = new TimeCountedRacer(raceController.Racers[i], raceController);
        }

        void attachText()
        {
            for (int i = 0; i < raceController.Racers.Count; ++i)
                raceController.Racers[i] = new NameDisplayedRacerCharacter(raceController.Racers[i], this);
        }

        void attachAI()
        {
            for (int i = 0; i < raceController.Racers.Count; ++i)
                if (!raceController.Racers[i].IsHuman)
                    raceController.Racers[i] = new AIRacerCharacter(raceController.Racers[i], raceController);
        }

        /// <summary>
        /// Resets the games
        /// </summary>
        public void Reset()
        {
            raceController = new RaceController(4);
            inputController = new InputController();
            loadRacers();
        }

        void delay(int milliseconds)
        {
            if (!delayEnabled)
            {
                delayMilliseconds = milliseconds;
                delayEnabled = true;
            }
        }

        /// <summary>
        /// Restores graphics device to its initial state for mixing 2D and 3D drawing
        /// </summary>
        void restoreGraphicsDevice()
        {
            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.None;
            GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
            GraphicsDevice.SamplerStates[0] = SamplerState.AnisotropicWrap;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Singleton object
        /// </summary>
        public static RaceGame Instance
        {
            get 
            {
                if (instance == null)
                {
                    instance = new RaceGame();
                }

                return RaceGame.instance; 
            }
        }

        /// <summary>
        /// Font used in game
        /// </summary>
        public SpriteFont Font
        {
            get { return font; }
            set { font = value; }
        }

        /// <summary>
        /// Sprite batch of the game
        /// </summary>
        public SpriteBatch SpriteBatch
        {
            get { return spriteBatch; }
            set { spriteBatch = value; }
        }

        public AudioManager AudioManager
        {
            get { return audioManager; }
            set { audioManager = value; }
        }

        public GraphicsDeviceManager Graphics
        {
            get { return graphics; }
            set { graphics = value; }
        }

        public CameraMode CameraMode
        {
            get { return cameraMode; }
            set { cameraMode = value; }
        }

        public Camera CurrentCamera
        {
            get { return currentCamera; }
            set { currentCamera = value; }
        }

        public InputController InputController
        {
            get { return inputController; }
            set { inputController = value; }
        }

        public RaceController RaceController
        {
            get { return raceController; }
            set { raceController = value; }
        }

        public TextDrawController TextDrawController
        {
            get { return textDrawController; }
            set { textDrawController = value; }
        }

        public List<StaticModel> StaticModels
        {
            get { return staticModels; }
            set { staticModels = value; }
        }

        public SkySphere SkySphere
        {
            get { return skySphere; }
            set { skySphere = value; }
        }

        public GameState GameState
        {
            get { return currentState; }
            set { currentState = value; }
        }

        public bool Paused
        {
            get { return paused; }
            set { paused = value; }
        }

        /// <summary>
        /// Gets or sets whether texts over the heads should be diplayed
        /// </summary>
        public bool ShowText
        {
            get { return showText; }

            set
            {
                if (showText != value)
                {
                    RacerCharacter ndrChar;

                    for (int i = 0; i < raceController.Racers.Count; ++i)
                    {
                        ndrChar = raceController.Racers[i];

                        while (ndrChar is NameDisplayedRacerCharacter == false)
                        {
                            ndrChar = (ndrChar as RacerDecorator).DecoratedRacerCharacter;
                        }

                        ((NameDisplayedRacerCharacter)ndrChar).Enabled = value;
                    }

                    showText = value;
                }
            }
        }

        #endregion

        #region Fields

        static RaceGame instance = null;
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        AudioManager audioManager;

        //BillboardSystem trees;

        TextDrawController textDrawController;
        InputController inputController;
        RaceController raceController;

        List<Camera> cameras = new List<Camera>();
        List<StaticModel> staticModels = new List<StaticModel>();

        RacerCharacter trackedRacer;
        int trackedRacerIndex = 0;

        bool paused = false;

        Camera currentCamera;
        CameraMode cameraMode = CameraMode.ChaseCamera;
        
        Model dude, hulki, gun, soldier, soccer;
        Texture2D raceTileAsset;

        Model skySphereModel;
        Effect skySphereEffect;
        SkySphere skySphere;

        SpriteFont font;

        GameState currentState;

        int delayMilliseconds;
        bool delayEnabled;

        bool showText = true;

        #endregion
    }

    #region Enums

    /// <summary>
    /// Camera Mode being used
    /// </summary>
    public enum CameraMode
    {
        ChaseCamera = 0,
        FreeCamera,
        ArcBallCamera,
        TargetCamera
    }

    #endregion
}
